CSVは「軽い・シンプル・どこでも開ける」ため、 業務システムのインポート・エクスポートで今も主役です。 一方で、10万件〜100万件クラスになると、 素直な実装ではメモリ不足・処理遅延・フリーズが発生します。
・C#でのCSV基本読み書き
・大量データを扱うときの「やってはいけない実装」
・ストリーミング処理(1行ずつ処理)
・分割出力・フィルタ・集計のパターン
・非同期処理と進捗表示
・業務アプリ向けベストプラクティス
1. CSV処理で「やってはいけない」典型パターン
大量CSVでまず避けるべきは、次のような実装です。
■ 1-1. 全行を一気にメモリに載せる
// 悪い例:100万行を全部メモリに読み込む
var lines = File.ReadAllLines("data.csv"); // メモリ圧迫
foreach (var line in lines)
{
// 処理...
}
10万行程度ならまだしも、100万行を超えると メモリ使用量が一気に跳ね上がり、GCも頻発します。
■ 1-2. 1行ごとにファイルを開閉する
// 悪い例:毎回Open/Close
foreach (var item in items)
{
File.AppendAllText("out.csv", item.ToCsvLine() + Environment.NewLine);
}
ファイルI/Oのオーバーヘッドで極端に遅くなります。
2. 基本:ストリーミングで1行ずつ処理する
大量CSVの基本は、「読みながら処理する」ストリーミング方式です。 メモリに全行を載せず、1行ずつ読みながら処理していきます。
■ 2-1. 読み込みの基本パターン
using var reader = new StreamReader("data.csv");
string? line;
while ((line = reader.ReadLine()) != null)
{
// 1行ずつ処理
var cols = line.Split(',');
// cols[0], cols[1], ...
}
この方式なら、100万行でもメモリ使用量はほぼ一定です。
■ 2-2. 書き込みの基本パターン
using var writer = new StreamWriter("out.csv", false); // 上書き
foreach (var item in items)
{
var line = string.Join(",", item.Id, item.Name, item.Amount);
writer.WriteLine(line);
}
1つのStreamWriterを使い回すことで、I/Oを最小限に抑えます。
3. ヘッダー付きCSVの処理
業務CSVでは、1行目にヘッダーがあるケースがほとんどです。 ヘッダーを読み飛ばす or 利用するパターンを押さえておきます。
■ 3-1. ヘッダーを読み飛ばす
using var reader = new StreamReader("data.csv");
// 1行目ヘッダーを読み飛ばす
reader.ReadLine();
string? line;
while ((line = reader.ReadLine()) != null)
{
var cols = line.Split(',');
// 処理...
}
■ 3-2. ヘッダーをキーにして列を参照する
using var reader = new StreamReader("data.csv");
var headerLine = reader.ReadLine() ?? "";
var headers = headerLine.Split(',');
// 列名 → インデックスのマップ
var index = headers
.Select((name, i) => new { name, i })
.ToDictionary(x => x.name, x => x.i);
string? line;
while ((line = reader.ReadLine()) != null)
{
var cols = line.Split(',');
var id = cols[index["Id"]];
var name = cols[index["Name"]];
var amt = decimal.Parse(cols[index["Amount"]]);
}
列順が変わる可能性があるCSVでは、この方式が安全です。
4. 大量CSVのフィルタ・集計処理
「条件に合う行だけ抽出したい」「合計値を出したい」 といった処理も、ストリーミングで十分対応できます。
■ 4-1. 条件フィルタして別CSVに出力
using var reader = new StreamReader("input.csv");
using var writer = new StreamWriter("filtered.csv", false);
var header = reader.ReadLine();
writer.WriteLine(header); // ヘッダーをそのままコピー
string? line;
while ((line = reader.ReadLine()) != null)
{
var cols = line.Split(',');
var amount = decimal.Parse(cols[3]); // 例:4列目が金額
if (amount >= 100000) // 10万円以上だけ出力
{
writer.WriteLine(line);
}
}
■ 4-2. 合計・件数などの集計
using var reader = new StreamReader("input.csv");
reader.ReadLine(); // ヘッダー読み飛ばし
decimal total = 0;
int count = 0;
string? line;
while ((line = reader.ReadLine()) != null)
{
var cols = line.Split(',');
var amount = decimal.Parse(cols[3]);
total += amount;
count++;
}
Console.WriteLine($"件数: {count}, 合計: {total:#,##0}");
この方式なら、100万行でもメモリをほとんど使わずに集計できます。
5. 大きなCSVを分割する(ファイル分割)
「1ファイルが大きすぎて扱いづらい」「メール添付できない」 といった場合は、行数で分割するのが定番です。
■ 5-1. N行ごとにファイル分割
int maxLinesPerFile = 50000; // 5万行ごとに分割
int fileIndex = 1;
int currentLineCount = 0;
using var reader = new StreamReader("big.csv");
string? header = reader.ReadLine();
StreamWriter? writer = null;
void OpenNewFile()
{
writer?.Dispose();
string fileName = $"big_part_{fileIndex:D3}.csv";
writer = new StreamWriter(fileName, false);
writer.WriteLine(header);
currentLineCount = 0;
fileIndex++;
}
OpenNewFile();
string? line;
while ((line = reader.ReadLine()) != null)
{
if (currentLineCount >= maxLinesPerFile)
{
OpenNewFile();
}
writer!.WriteLine(line);
currentLineCount++;
}
writer?.Dispose();
これで「1ファイルあたり5万行」の分割CSVが生成できます。
6. 非同期処理と進捗表示(WPF / コンソール)
大量CSV処理は時間がかかるため、 UIスレッドをブロックしない非同期実行が重要です。
■ 6-1. 非同期でCSV処理を実行
public async Task ProcessCsvAsync(string path, IProgress<int>? progress = null)
{
var totalLines = File.ReadLines(path).Count(); // ざっくり行数取得
int processed = 0;
using var reader = new StreamReader(path);
reader.ReadLine(); // ヘッダー
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
// 行の処理...
processed++;
if (processed % 1000 == 0)
{
int percent = (int)(processed * 100.0 / totalLines);
progress?.Report(percent);
}
}
}
■ 6-2. WPFでの進捗表示イメージ
public async Task RunAsync()
{
IsBusy = true;
var progress = new Progress<int>(p => Progress = p);
await _csvService.ProcessCsvAsync("data.csv", progress);
IsBusy = false;
}
「どれくらい進んでいるか」が見えるだけで、ユーザーのストレスは大きく減ります。
7. CSVライブラリを使うかどうかの判断
素の Split(',') でも十分ですが、
カンマ・ダブルクォート・改行を含むフィールドを正しく扱うには
専用ライブラリ(CsvHelperなど)を使うのが安全です。
■ ライブラリを使うべきケース
- Excel由来のCSV(セル内改行・カンマ含む)
- ダブルクォートで囲まれたフィールド
- 複数のCSVフォーマットに対応する必要がある
一方で、シンプルな社内CSVなら、素の実装でも十分なことが多いです。
8. 業務アプリ向けベストプラクティス
- 大量CSVは必ずストリーミング処理(ReadLine)で扱う
- 全行をメモリに載せない(ReadAllLines禁止)
- 1つのStreamWriterを使い回して書き込む
- ヘッダー行の扱い(読み飛ばす or 列名マップ)を明確にする
- 分割出力・フィルタ・集計は「読みながら」行う
- 時間がかかる処理は非同期+進捗表示
- 複雑なCSV形式には専用ライブラリを検討する
まとめ:CSV高速処理は“メモリを信じず、流しながら処理する”が正解
- 大量CSVは「全部読む」ではなく「流しながら処理」が基本
- ストリーミング+非同期で、100万行でも安定して処理できる
- 分割・フィルタ・集計を組み合わせると、現場の手作業が一気になくなる
「CSVが重くて開けない」「アプリが固まる」 という現場の悩みに対して、 C#のストリーミング処理は非常に強力な解決策になります。 この記事をベースに、あなたの業務アプリに最適なCSV処理フローを設計してみてください。